[TOC]

1 引言

在前面高性能代码优化规则参考一文中,我们举了一个优化的例子;但是,但是,结果优化变成劣化(恶化了),演砸了。我们继续看看到底是怎么回事?

2 优化规则评价

先复原一下场景。

光说不练假把式,每种语言运行环境都有各自的表达方式,这里用“最简单“的JavaScript做一个演示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
var i,len,testData = [],testCount = 9999,repeatCount = 999;

function doInit(testData){
for(var i = testCount; i--;){
testData[i] = i;
}
}

function doRepeat(operation,data){
for(var i = 0; i < repeatCount;i++){
operation(data);
}
}
function origin(data){
var ret = [];
for(var i = 0; i < data.length; i++){
ret[i] = data[i] * data[i];
}
}

function optLen(data){
var ret = [];
for(var i = 0,len = data.length; i < len; i++){
ret[i] = data[i] * data[i];
}
}

function optIdx(data){
var ret = [],tmpData;
for(var i = 0,len = data.length; i < len; i++){
tmpData = data[i];
ret[i] = tmpData * tmpData;
}
}

function optCmp(data){
var ret = [];
for(var i = data.length; i-- > 0;){
ret[i] = data[i] * data[i];
}
}

doInit(testData);

console.time("origin")
doRepeat(origin,testData);
console.timeEnd('origin')

console.time('optmize len');
doRepeat(optLen,testData)
console.timeEnd('optmize len');

console.time('optmize index');
doRepeat(optIdx,testData)
console.timeEnd('optmize index');

console.time('optmize compare');
doRepeat(optCmp,testData)
console.timeEnd('optmize compare');

多次执行上面的代码查看效果,结果如下:

Safari 10.1 (12603.1.30.0.34) 下的运行结果(基本稳定):

1
2
3
4
origin: 163.225ms
optmize len: 139.275ms
optmize index: 118.999ms
optmize compare: 248.611ms

Chrome 58.0.3029.96 下的运行结果(不太稳定):

1
2
3
4
origin: 81.718017578125ms
optmize len: 77.73486328125ms
optmize index: 74.80517578125ms
optmize compare: 378.320068359375ms

NodeJS 7.9.0下的运行结果(不太稳定):

1
2
3
4
origin: 110.480ms
optmize len: 104.123ms
optmize index: 98.463ms
optmize compare: 383.104ms

上面结果看来,优化循环判断条件和数据缓存都有一定的效果;但最后一种风骚的写法,看起来少了一个局部变量,但执行结果是成倍的劣化(恶化)。

这曾经也是一种代码优化的方式,作者也深中其毒,但这已经不那么重要了。现在问题来了,为什么会这么劣化,是一个值得研究的问题。

3 问题深入研究

3.1 找工具

  • 搜索v8 perf tool Google 第一条得到一个工具列表
  • 筛选掉其中内存和CPU相关的,剩下irhydra可以做中间代码分析的
  • 我们就是想看看咱们的源码被转成什么样的代码来执行了

3.2 搞出来看一看

  • node 本身是基于v8的,所以也不用各种下载v8源码编译什么的了,直接上
  • 使用node 生成irhydra需要的内容
  • 将上面的源代码存为perf-opt.js
  • 执行下面的命令
1
node --trace-hydrogen --trace-phase=Z --trace-deopt --code-comments --hydrogen-track-positions --redirect-code-traces --redirect-code-traces-to=code.asm --print-opt-code perf-opt.js
  • IRHydra2的界面导入上面生成的.asm.cfg文件
  • 可以看到我们四个函数分别对应的中间代码了

3.3 找文档 捋主线

  • 搜索v8 intermediate representations 在得到的结果中可以得到如下的概念
    • Crankshaft是v8的编译优化器
    • HydrogenCrankshaft中的HIR(High-Level intermediate representations)中间代码,更接近于源代码,跟机器无关
    • LithiumCrankshaft中的LIR(Low-Level intermediate representations)中间代码,更接近于机器码,跟机器相关
    • SSA(Static Single Assignment)Hydrogen的描述格式,是一种组织IR(intermediate representations)的方式。
  • 搜索Static Single Assignment可以得到一些关于SSA的参考
  • 参考资料
    • 《Compiled Compiler Templates for V8》
    • 《Static Single Assignment Book》
  • 没有找到更详尽的关于SSA的文档,比如SSA的指令及其含义说明,只有v8源码中的src/crankshaft/hydrogen-instructions,如果你找到了,请不吝赐教(怎么找的?)。

3.4 中间代码分析

3.4.1 工具介绍
  • Load Compilation Atrifacts 加载.asm.cfg文件
  • IR 标签页,打开眼睛可以看到源代码和中间代码的映射关系,深红色的竖条表示循环范围
  • Graph 标签页,打开问号可以查看控制流图的说明,点击图块可以跳转到IR标签页
3.4.2 总体结果
  • 前三段代码(origin,optLen,optIdx)的控制流图完全一致,B3-B8是循环内容
  • 第三段代码(optCmp)与其他三段差别较大,
3.4.3 详细对比
3.4.3.1 origin 与 optLen的区别
  • origin比optLen在B2块少一个LoadNamedField t2.%length@24 Smi 获取数组长度,代号i33,在循环内容之外
  • origin比optLen在B3块多一个LoadNamedField t2.%length@24 Smi 获取数组长度,代号i42,每次循环都有这个操作
  • origin比optLen在B5块少一个LoadNamedField t2.%length@24 Smi 获取数组长度,代号i62,每次循环都有,这个地方不科学(mark),需要用i62的地方直接使用i33就ok
  • 其他操作无差异
3.4.3.2 optLen与optIdx的区别
  • optLen比optIdx在B5块多一个var[4] = t106赋值,但这个赋值后面好像没用上。。。
3.4.3.3 optCmp与optLen比较的异常
  • 获取data[i]的时候多了CheckMaps t2 [0x12044e7ace69](stability-check)操作
  • 存储乘积结果的时候多了CallWithDescriptor t76 t7 t16 t86 t87 s88 t74 #0 changes[*] Tagged
  • 异常的描述(这是代码未被编译器优化识别的警告信息)
1
2
3
This instruction has side effects **unknown** to the compiler.
It can change hidden classes of objects, kind of array backing stores, etc.
Such instructions should be avoided inside hot loops because they inhibit optimizations.

3.5 看起来还没有找到想要的

  • 上面的SSA本身存在问题,参考意义不大
  • 没有比较出前面三个代码之间的差异
  • 最后一个优化搞砸了还是没解释清楚

4 重头再来

4.1 使用PHP来实现(为什么?)

  • 在这个例子上,PHP跟JavaScript极其相似
  • PHP是世界上最好的语言(别打脸~~)
  • PHP下用VLD 查看opcode我玩过一次,比SSA看起来更细致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?php

define('TESTCOUNT',9999);
define('REPEATCOUNT',999);
define('PRECISION',8);

function mstime_str()
{
list($usec, $sec) = explode(" ", microtime());
//不丢失系统原有精度
return (bcadd($usec,$sec,PRECISION));
}

function get_exec_time($func,$param){
$start = mstime_str();
call_user_func(do_repeat, $func,$param);
$end = mstime_str();
return bcsub($end,$start,PRECISION);
}


function do_init(&$arr){
for($i = TESTCOUNT; $i--;){
$arr[$i] = $i;
}
}

function do_repeat($operation,$data){
for($i = 0; $i < REPEATCOUNT;$i++){
$operation($data);
}
}
function origin($data){
$ret = array();
for($i = 0; $i < count($data); $i++){
$ret[$i] = $data[$i] * $data[$i];
}
}

function opt_len($data){
$ret = array();
for($i = 0,$len = count($data); $i < $len; $i++){
$ret[$i] = $data[$i] * $data[$i];
}
}

function opt_idx($data){
$ret = array();
for($i = 0,$len = count($data); $i < $len; $i++){
$tmp = $data[$i];
$ret[$i] = $tmp * $tmp;
}
}

function opt_cmp($data){
$ret = array();
for($i = count($data); $i--;){
$ret[$i] = $data[$i] * $data[$i];
}
}


$test_data = array();

do_init($test_data);
echo 'origin:'.(get_exec_time(origin,$test_data)).PHP_EOL;
echo 'opt_len:'.(get_exec_time(opt_len,$test_data)).PHP_EOL;
echo 'opt_idx:'.(get_exec_time(opt_idx,$test_data)).PHP_EOL;
echo 'opt_cmp:'.(get_exec_time(opt_cmp,$test_data)).PHP_EOL;

?>

4.2 执行上面的PHP代码

  • 将上面的内容存为perf-opt.php
  • 运行php -f perf-opt.php
  • PHP 5.6.30 上面的执行结果(稳定)
1
2
3
4
origin:4.74019000
opt_len:3.53197500
opt_idx:3.35585800
opt_cmp:2.95980300

4.3 也可以使用node自带的工具来看

  • 执行命令
1
node --prof perf-opt.js
  • 转换结果
1
node --prof-process isolate-xxx-v8.log > processed.log
  • 查看processed.log

5 结论

  • 某些优化规则在不同的编译环境下,不一定能得到一致的效果
  • 规则是死的,但编译系统不断在改变和发展
  • 有可能我们的优化规则,跟编译器的优化冲突了,成了执行优化的障碍

6 参考资料

  1. 《Compiled Compiler Templates for V8》

  2. 《Static Single Assignment Book》

  3. V8 perf tool

  4. V8源码

  5. IRHydra

  6. IRHydra github

  7. v8: a tale of two compilers

  8. Optimizing for V8 - Hydrogen

  9. v8-bailout-reasons

  10. Optimization-Killers

  11. A tour of V8: Crankshaft, the optimizing compiler

  12. V8 之旅:优化编译器

  13. [WebKit] JavaScriptCore解析–高级篇(一) SSA (static single assignment)

  14. GNU Compiler Collection (GCC) Internals

  15. Analysis and Optimization of GIMPLE tuples

  16. static single assignment for functional programmers

  17. a closer look at crankshaft, v8’s optimizing compiler

最后更新: 2022年03月02日 03:32

原始链接: http://rawbin-.github.io/performance/2017-05-07-code-optmization-ext/

× 赞赏这个人~
打赏二维码